package biback.utils

import cats.Monad
import cats.data.{EitherT, Ior}

import scala.reflect.ClassTag

abstract class DomainEntity {

  type Command
  type Event
  type State
  type CommandResult = Either[DomainError, Ior[Any, Event]]

  def commandHandler(s: State, c: Command): CommandResult

  def applyEvent(s: State, e: Event): State

  def applyCreateEvent(e: Event): State

  var state: Option[State] = None
  var entityId: String = "?"

  private def doAsk[C <: Command with ReplyType[_]](command: C): Either[DomainError, Any] =
    for {
      s <- state.toRight(EntityEmpty(entityId))
      o <- commandHandler(s, command).map { oe =>
        println(oe)
        oe.map(e => state = Some(applyEvent(s, e)))
        oe.left.getOrElse(()).asInstanceOf[command.ReplyType]
      }
    } yield o

  final def ask[F[_], C <: Command with ReplyType[_]](command: C)
                                                     (implicit F: Monad[F]): EitherT[F, DomainError, command.ReplyType] =
    EitherT.fromEither[F](doAsk(command).asInstanceOf[Either[DomainError, command.ReplyType]])


  final def create[F[_]](e: Event)
                        (implicit F: Monad[F]): EitherT[F, DomainError, Unit] =
    for {
      _ <- EitherT.fromEither[F](Either.cond(state.isEmpty, (), EntityExisted(entityId)))
      _ <- EitherT.rightT[F, DomainError](state = Some(applyCreateEvent(e)))
    } yield ()

  final def found[F[_]](implicit F: Monad[F]): EitherT[F, DomainError, State] =
    EitherT.fromOption[F](state, EntityEmpty(entityId))

  final def replay(events: List[Event]): Unit = {
    if (events.nonEmpty) {
      state = Some(applyCreateEvent(events.head))
      events.drop(1).foreach(e => state = Some(applyEvent(state.get, e)))
    }
  }
}

trait RegistryAlgebra[F[_]] {
  val registry: RegistryAlgebra.Service[F]
}

object RegistryAlgebra {

  trait Service[F[_]] extends GeneralAlgebra[F] {
    def refFor[P <: DomainEntity : ClassTag](entityId: String)(implicit F: Monad[F]): ErrOrF[P]

    def read[P <: DomainEntity : ClassTag](entityId: String)(implicit F: Monad[F]): ErrOrF[P#State] =
      for {
        p <- refFor[P](entityId)
        s <- p.found
      } yield s
  }

}

trait ReplyType[R] {
  type ReplyType = R
}
